Query Execution Process
contents
SQL 쿼리의 "실행(Run)" 버튼을 누르면 결과가 즉시 나오는 것처럼 보이지만, 그 이면에서는 데이터베이스 엔진이 논리, 수학, 그리고 I/O(입출력) 작업의 복잡한 과정을 수행합니다. 이 과정은 크게 5단계로 나뉩니다.
1. 연결 계층 (The Connection Layer - 핸드셰이크)
쿼리가 엔진에 도달하기 전, 클라이언트(애플리케이션 또는 SQL 툴)는 데이터베이스와 연결해야 합니다.
- 프로토콜 확인: 데이터베이스는 통신 프로토콜(보통 TCP/IP)을 검증합니다.
- 인증 (Authentication): 사용자 이름과 비밀번호를 확인합니다.
- 권한 부여 (Authorization): 해당 데이터베이스에 접근할 권한이 있는지 확인합니다.
- 스레드 할당: 데이터베이스는 사용자의 연결을 처리할 특정 스레드(또는 프로세스)를 할당합니다.
참고: 이 과정 때문에 커넥션 풀링(Connection Pooling) 이 중요합니다. 쿼리마다 매번 새로운 연결을 만드는 것은 비용이 많이 듭니다. 풀링은 기존 연결을 재사용하여 이 단계를 건너뜁니다.
2. 파서 (The Parser - 번역가)
데이터베이스는 영어와 유사한 SQL 문장을 바로 이해하지 못합니다. 이를 분해해야 하는데, 파서가 두 가지 주요 작업을 수행합니다.
A. 구문 체크 (Syntax Check - 문법)
SQL이 문법적으로 올바르게 작성되었는지 확인합니다.
- 예시:
SELECT대신SELECR라고 썼는가? 닫는 괄호를 빠뜨렸는가? - 실패하면 즉시 "Syntax Error(구문 오류)"를 반환하고 멈춥니다.
B. 의미 체크 (Semantics Check - 의미)
문법이 맞아도, 쿼리의 내용이 말이 되는지 확인합니다.
- 테이블이 존재하는가? (
SELECT * FROM ghosts) - 컬럼이 존재하는가?
- 모호성 확인: 두 테이블을 조인하는데 둘 다
id컬럼이 있다면, 어떤 테이블의id인지 명시했는가 (TableA.id)?
출력물: 이 단계의 결과로 쿼리의 논리적 단계를 나타내는 계층적 구조인 파스 트리(Parse Tree) 가 생성됩니다.
3. 쿼리 옵티마이저 (The Query Optimizer - 두뇌)
RDBMS에서 가장 복잡하고 중요한 부분입니다. SQL은 선언적 언어(Declarative Language) 입니다(어떻게 할지가 아니라, 무엇 을 원하는지만 말함). 어떻게 가져올지는 옵티마이저가 결정합니다.
옵티마이저는 비용 기반 최적화(CBO) 를 사용하여 여러 잠재적인 "실행 계획"을 만들고, 각 계획의 "비용"(CPU 사이클 + 디스크 I/O)을 계산하여 가장 저렴한 것을 선택합니다.
옵티마이저의 주요 결정 사항:
- 접근 방법 (Access Methods - 데이터 가져오는 법):
- Full Table Scan: 모든 행을 다 읽음 (작은 테이블에 적합).
- Index Seek/Scan: B-Tree 인덱스를 타고 특정 데이터로 바로 점프 (특정
WHERE조건이 있는 큰 테이블에 적합).
- 조인 방법 (Join Methods - 테이블 합치는 법):
- Nested Loop Join: A 테이블을 루프 돌면서 매번 B 테이블을 검색 (작은 데이터셋).
- Hash Join: 작은 테이블로 메모리에 해시 맵을 만든 뒤, 큰 테이블과 비교 (대용량 데이터셋).
- Merge Join: 두 테이블을 먼저 정렬(Sort)한 뒤 지퍼처럼 합침.
- 조인 순서 (Join Order):
- A, B, C 테이블을 조인할 때, A+B를 먼저 할지 B+C를 먼저 할지 결정합니다. 순서에 따라 성능 차이가 큽니다.
결정의 근거: 옵티마이저는 이러한 수학적 추측을 위해 통계 정보(Statistics)(행의 개수, 데이터 분포도, 히스토그램 등)를 사용합니다.
출력물: 실행 계획(Execution Plan) (구체적인 작업 지시서).
4. 쿼리 실행기 (The Query Executor - 실행/근육)
실행기는 실행 계획을 받아 실제로 수행합니다. 일종의 관리자(Coordinator) 역할을 합니다.
- 디스크에 직접 접근하지 않고, 스토리지 엔진 API와 통신합니다.
- 볼케이노 모델(Volcano Model): 대부분의 DB는 파이프라인 모델을 사용합니다. 실행기는 하위 노드에게 데이터를 요청("Get Next Row")합니다. 이 요청은 트리의 맨 아래(테이블)까지 내려가고, 데이터는 다시 위로 올라오면서 필터링, 조인, 정렬됩니다.
5. 스토리지 엔진 (The Storage Engine - 파일 캐비닛)
실제 물리적 데이터가 존재하는 곳입니다. (MySQL의 InnoDB 등). 스토리지 엔진은 데이터의 실제 조회 및 관리를 담당합니다.
A. 버퍼 풀 확인 (캐싱)
디스크를 보기 전에, 엔진은 버퍼 풀(Buffer Pool - RAM) 을 확인합니다.
- Hit: 데이터 페이지가 이미 RAM에 있다면 즉시 반환 (나노초 단위).
- Miss: 없다면 디스크에서 가져와야 함 (밀리초 단위 - 훨씬 느림).
B. 디스크 I/O
데이터가 디스크에 있다면, 엔진은 "페이지(Page)"(보통 8KB or 16KB 블록) 단위로 데이터를 읽어 버퍼 풀에 적재합니다.
C. 동시성 제어 (잠금)
읽는 동안 엔진은 ACID 속성을 보장합니다.
- 데이터를 읽는 동안 수정되지 않도록 "공유 잠금(Shared Lock)"을 걸 수 있습니다.
- 트랜잭션이 데이터를 수정 중이라면, Undo Log (MVCC)를 확인하여 올바른 버전 의 데이터를 읽도록 보장합니다.
요약 흐름도
- Client: SQL 전송 (
SELECT * FROM Users...). - Parser: 철자/테이블 확인 -> 파스 트리 생성.
- Optimizer: 수학/통계 계산 -> 실행 계획 생성.
- Executor: 계획에 따라 수행 -> 스토리지 엔진에 데이터 요청.
- Storage Engine: RAM(버퍼 풀) 확인 -> 디스크 읽기 -> 행(Row) 반환.
- Client: 결과 집합(Result Set) 수신.
왜 이것을 알아야 할까요?
이 과정을 이해하면 느린 쿼리를 디버깅할 수 있습니다.
- Parse가 느리다? 쿼리 텍스트가 너무 길거나 뷰(View)가 너무 많을 수 있습니다.
- Plan(계획)이 나쁘다? 통계 정보(Statistics) 가 오래되어 옵티마이저가 인덱스 대신 "Full Table Scan"을 선택했을 수 있습니다.
- I/O가 느리다? 버퍼 풀이 너무 작아서 디스크 읽기가 너무 빈번하게 발생할 수 있습니다.
references